package sceneGraph;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.vecmath.*;

import utils.CubicMapping;

import com.sun.opengl.util.texture.*;

public class PlanetMesh implements ISceneObject {

	private List<PlanetTriangle> subFaces;
	private float radius;

	private CubicMapping heightMap;
	private Texture texture;

	private Map<Point3f, List<PlanetTriangle>> verticesDB = new HashMap<Point3f, List<PlanetTriangle>>();
	private Map<Point3f, Point3f> verticesCache = new HashMap<Point3f, Point3f>();
	private Map<Point3f, Vector3f> normals = new HashMap<Point3f, Vector3f>();

	public PlanetMesh(float radius, int subDevSize, String textureFilePath, String heightMapFilePath) {

		float X = 0.525731112119133606f * radius;
		float Z = 0.850650808352039932f * radius;

		Point3f baseVertices[] = { new Point3f(-X, 0.0f, Z), new Point3f(X, 0.0f, Z), new Point3f(-X, 0.0f, -Z), new Point3f(X, 0.0f, -Z),
				new Point3f(0.0f, Z, X), new Point3f(0.0f, Z, -X), new Point3f(0.0f, -Z, X), new Point3f(0.0f, -Z, -X), new Point3f(Z, X, 0.0f),
				new Point3f(-Z, X, 0.0f), new Point3f(Z, -X, 0.0f), new Point3f(-Z, -X, 0.0f) };

		List<PlanetTriangle> baseFaces = new ArrayList<PlanetTriangle>();
		baseFaces.add(new PlanetTriangle(baseVertices[0], baseVertices[4], baseVertices[1]));
		baseFaces.add(new PlanetTriangle(baseVertices[0], baseVertices[9], baseVertices[4]));
		baseFaces.add(new PlanetTriangle(baseVertices[9], baseVertices[5], baseVertices[4]));
		baseFaces.add(new PlanetTriangle(baseVertices[4], baseVertices[5], baseVertices[8]));
		baseFaces.add(new PlanetTriangle(baseVertices[4], baseVertices[8], baseVertices[1]));
		baseFaces.add(new PlanetTriangle(baseVertices[8], baseVertices[10], baseVertices[1]));
		baseFaces.add(new PlanetTriangle(baseVertices[8], baseVertices[3], baseVertices[10]));
		baseFaces.add(new PlanetTriangle(baseVertices[5], baseVertices[3], baseVertices[8]));
		baseFaces.add(new PlanetTriangle(baseVertices[5], baseVertices[2], baseVertices[3]));
		baseFaces.add(new PlanetTriangle(baseVertices[2], baseVertices[7], baseVertices[3]));
		baseFaces.add(new PlanetTriangle(baseVertices[7], baseVertices[10], baseVertices[3]));
		baseFaces.add(new PlanetTriangle(baseVertices[7], baseVertices[6], baseVertices[10]));
		baseFaces.add(new PlanetTriangle(baseVertices[7], baseVertices[11], baseVertices[6]));
		baseFaces.add(new PlanetTriangle(baseVertices[11], baseVertices[0], baseVertices[6]));
		baseFaces.add(new PlanetTriangle(baseVertices[0], baseVertices[1], baseVertices[6]));
		baseFaces.add(new PlanetTriangle(baseVertices[6], baseVertices[1], baseVertices[10]));
		baseFaces.add(new PlanetTriangle(baseVertices[9], baseVertices[0], baseVertices[11]));
		baseFaces.add(new PlanetTriangle(baseVertices[9], baseVertices[11], baseVertices[2]));
		baseFaces.add(new PlanetTriangle(baseVertices[9], baseVertices[2], baseVertices[5]));
		baseFaces.add(new PlanetTriangle(baseVertices[7], baseVertices[2], baseVertices[11]));

		for (int i = 0; i < baseVertices.length; i++) {
			addUniqueToDb(baseVertices[i]);
		}
		for (PlanetTriangle t : baseFaces) {
			addTriangleToDb(t);
		}

		this.subFaces = baseFaces;
		this.radius = radius;

		subdivide(subDevSize);

		heightMap = new CubicMapping(heightMapFilePath);

		// Save all vertices to a list
		List<Point3f> allVertices = new ArrayList<Point3f>(verticesDB.keySet());

		// Scale all vertices according to heightmap, and recreate the hashmap value
		for (Point3f p : allVertices) {
			List<PlanetTriangle> triangleList = verticesDB.remove(p);
			p.scale(2.0f * heightMap.getHeightAtPoint(p));
			verticesDB.put(p, triangleList);
		}

		// Calculate normal for each vertices
		for (Point3f p : allVertices) {
			Vector3f normal = new Vector3f(0, 0, 0);
			for (PlanetTriangle t : verticesDB.get(p)) {
				normal.add(t.getNormal());
			}
			normal.normalize();

			normals.put(p, normal);
		}

		// Update triangles normal
		for (PlanetTriangle t : subFaces) {
			t.setNormals(normals.get(t.getVertex1()), normals.get(t.getVertex2()), normals.get(t.getVertex3()));
		}

		// load texture to glu
		try {
			texture = TextureIO.newTexture(getClass().getResourceAsStream(textureFilePath), true, null);
		} catch (Exception e) {
			e.printStackTrace();
		}

	}

	public List<PlanetTriangle> getFaces() {
		return subFaces;
	}

	@Override
	public void accept(Visitor visitor) {
		visitor.visit(this);
	}

	private void subdivide(int subDevSize) {
		for (int i = 0; i < subDevSize; i++) {
			int origSize = subFaces.size();
			for (int j = 0; j < origSize; j++) {
				// Get original triangle's vertices
				PlanetTriangle origTriangle = subFaces.remove(0);
				Point3f origVertex1 = origTriangle.getVertex1();
				Point3f origVertex2 = origTriangle.getVertex2();
				Point3f origVertex3 = origTriangle.getVertex3();
				removeTriangleToDb(origTriangle);

				// Create gentler vertices out of the original triangle
				Point3f pp12 = createGentlerVertex(origVertex1, origVertex2);
				Point3f pp23 = createGentlerVertex(origVertex2, origVertex3);
				Point3f pp31 = createGentlerVertex(origVertex3, origVertex1);
				pp12 = addUniqueToDb(pp12);
				pp23 = addUniqueToDb(pp23);
				pp31 = addUniqueToDb(pp31);

				// Add the new triangle to the DB, along with the new vertices
				PlanetTriangle newTriangle1 = new PlanetTriangle(pp12, pp23, pp31);
				subFaces.add(newTriangle1);
				addTriangleToDb(newTriangle1);

				// Add 3 new triangles "smaller" triangles
				PlanetTriangle newTriangle2 = new PlanetTriangle(origVertex1, pp12, pp31);
				PlanetTriangle newTriangle3 = new PlanetTriangle(origVertex2, pp23, pp12);
				PlanetTriangle newTriangle4 = new PlanetTriangle(origVertex3, pp31, pp23);
				subFaces.add(newTriangle2);
				subFaces.add(newTriangle3);
				subFaces.add(newTriangle4);
				addTriangleToDb(newTriangle2);
				addTriangleToDb(newTriangle3);
				addTriangleToDb(newTriangle4);
			}
		}
	}

	private Point3f createGentlerVertex(Point3f p1, Point3f p2) {
		Vector3f p12 = new Vector3f(p1);
		p12.add(p2);
		p12.normalize();
		p12.scale(radius);

		return new Point3f(p12);
	}

	private Point3f addUniqueToDb(Point3f p) {
		Point3f cache = verticesCache.get(p);
		if (cache == null) {
			verticesCache.put(p, p);
			cache = p;
			verticesDB.put(p, new ArrayList<PlanetTriangle>());
		}
		return cache;
	}

	private void addTriangleToDb(PlanetTriangle t) {
		verticesDB.get(t.getVertex1()).add(t);
		verticesDB.get(t.getVertex2()).add(t);
		verticesDB.get(t.getVertex3()).add(t);
	}

	private void removeTriangleToDb(PlanetTriangle t) {
		verticesDB.get(t.getVertex1()).remove(t);
		verticesDB.get(t.getVertex2()).remove(t);
		verticesDB.get(t.getVertex3()).remove(t);
	}

	public float getHeightAtPoint(Point3f pos) {
		return heightMap.getHeightAtPoint(pos);
	}

	public Texture getTexture() {
		return texture;
	}

}
